將敏感資訊與環境變數從代碼中抽離,實現安全管理。
寫程式時經常需要資料庫連線字串、API URL、密碼。如果寫死在程式碼裡,不僅改起來麻煩,還有嚴重的資安外洩風險——只要 repo 一公開(或被離職同事帶走),你的資料庫密碼就跟著公開了。
K8S 給了兩個工具:非敏感的用 ConfigMap,敏感的用 Secret。
我們用 ConfigMap 統一管理整個 Wafer BI 的環境變數(在 Helm 裡由 values.yaml 的 appConfig 區塊渲染):
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: k8sdemo
data:
POSTGRES_HOST: "postgres-service"
WAFER_BI_HOST: "wafer-backend-svc.k8sdemo.svc.cluster.local"
API_GATEWAY_PORT: "8080"
NODE_ENV: "production"
這些值會注入到微服務的環境變數中。後端架構改了(例如換 DB host),只要改這一個地方,不用動任何程式碼、不用重建任何 image。
密碼類的資訊用 Secret 資源。實際操作一次,順便揭穿一個常見的誤解:

▲ ConfigMap 與 Secret 實測:get secret 看到 base64「亂碼」,一行 base64 -d 直接還原明文
注意看最後兩步:kubectl get secret -o yaml 顯示的值長得像亂碼,很多人以為這樣就安全了。天真。 base64 是「編碼」不是「加密」,一行 base64 -d 就原形畢露。
所以 Secret 真正的保護來自:
get secret 的人本來就該是管理員而它不能保護的是:把 Secret YAML 直接 commit 進 Git——base64 擋不住任何人。
這就尷尬了:Day 17 之後我們要走 GitOps,「一切設定都進 Git」,但 Secret 進 Git 等於裸奔。哭啊。
解法是 Sealed Secrets:用叢集裡的私鑰做非對稱加密,Git 裡放的是加密後的 SealedSecret,只有目標叢集能解開。你可以放心把它推上公開 repo,攻擊者拿到也只是一坨真正的密文。
裝起來只有兩行——controller 進叢集,kubeseal 進本機:
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.38.4/controller.yaml
# kubeseal 從同一個 release 頁面下載對應平台的執行檔
controller 一啟動就會自己產生一組 RSA 金鑰對,公鑰給你封裝用,私鑰留在叢集裡。實際跑一次:

▲ Sealed Secrets 完整流程:本機封裝 → 密文進 Git → 叢集自動解密 → 刪掉明文還會自己補回來
流程拆解:
--dry-run=client -o yaml)。這份檔案是唯一有明文的地方,用完就刪kubeseal 用叢集公鑰封裝,吐出 SealedSecret。截圖裡那串 AgAAVYWDD4rD7H84... 就是真正的密文,792 個字元——這份可以安心進公開 repo
kubectl apply 之後,叢集裡同時存在兩個東西:我 apply 的 SealedSecret(密文),以及 controller 自動解出來的一般 Secret(明文,4 個鍵)envFrom: secretRef 完全不用改最後兩段是我覺得最能說明它性質的部分:
SealedSecret 才是真相來源,明文 Secret 只是它的產物(ownerReferences 指向 SealedSecret/app-secrets)。這跟 Day 17 的 ArgoCD selfHeal 是同一種思維:你手動改的東西不是真相,Git 裡的才是
kube-system 的一個 Secret 裡(截圖最後一行的 sealed-secrets-keyv62s8)。這句話有兩個意思:一是攻擊者拿到你的 repo 也解不開,二是這把私鑰掉了,所有封過的東西就都廢了——它才是真正要備份的資產(Day 27 講災難復原時會再遇到)。還有一個常見的坑要先講:密文跟「某一座叢集的金鑰」綁定。你在本機封的 SealedSecret 搬到 OKE 上是解不開的,必須拿新叢集的公鑰重新封一次。這是它的安全性來源,不是 bug。
順帶回收 Day 7 的伏筆:JWT_SECRET 除了要保密,還要夠長(HMAC-SHA 至少 256 bits)。放進 Secret 之前先確認長度,不然部署上去 Java 端直接 WeakKeyException 給你看。
Sealed Secrets 解決的是「設定怎麼安全地待在 Git 裡」。但有一類東西,連「存在叢集裡」都嫌太危險——例如簽發授權的私鑰。它一旦外流,別人就能無限發放合法的 License,而且你無從追查。
這時候要的不是加密儲存,是根本不要把金鑰交出去。這就是 KMS(Key Management Service)在做的事,我們專案用的是 OpenBao(HashiCorp Vault 的開源分支)的 Transit Secrets Engine:
// services/license-service:VaultService.java
transit.createKey(KEY_NAME, VaultTransitKeyCreationRequest.ofKeyType("rsa-4096"));
public String sign(String data) {
// 把資料送進去,拿簽章出來——私鑰從頭到尾沒有離開 OpenBao
return transit.sign(KEY_NAME, Plaintext.of(plaintextBase64)).getSignature();
}
關鍵是 transit.sign():運算發生在 KMS 內部,應用程式只拿得到結果,拿不到金鑰本身。就算 license-service 整個被攻破、記憶體被 dump,攻擊者也拿不到那把 rsa-4096——他最多只能在被發現之前多簽幾張 License。
搭配 K8S 的完整鏈路長這樣:
license-service ──sign()──► OpenBao Transit(rsa-4096 私鑰不出境)
│
└── GET /public-key ──► user-service 用公鑰驗章
只有公鑰會流出去,而公鑰本來就是公開的。
這是我覺得最值得記住的一張對照:
| Sealed Secrets | KMS / Transit | |
|---|---|---|
| 保護對象 | 靜態的設定值 | 不該落地的金鑰 |
| 金鑰在哪 | 解密後成為叢集裡的明文 Secret | 從不離開 KMS |
| 應用程式拿得到什麼 | 拿得到明文(它需要拿來連 DB) | 只拿得到運算結果(簽章、密文) |
| 適合 | POSTGRES_PASSWORD、JWT_SECRET、API Key |
簽章私鑰、加解密主金鑰 |
| 代價 | 幾乎零,只多一個 controller | 多一個必須高可用的服務,每次運算都要網路往返 |
判斷方式很簡單:問「應用程式需不需要看到這個值本身?」
兩者不衝突,而且常常一起用。實務上很典型的組合是:用 Sealed Secrets 保管「連線到 KMS 的 token」,再用 KMS 保管真正的金鑰——我們的 VAULT_TOKEN 走的就是這條路。這樣最敏感的資產只有一份,而且鎖在專門的服務裡。
誠實補充:這篇示範的 OpenBao 是跑在叢集裡的 dev 模式,開機就解封、token 寫死成
root,只夠展示 Transit 的運作方式。正式環境要處理的還有 unseal 流程、token 輪替、以及「OpenBao 掛掉時 License 就簽不出來」的高可用問題——那是另一個 30 天的題目了。
設定與密碼都各就各位了,而且分成三層:ConfigMap 放不敏感的、Sealed Secrets 讓敏感設定能安全地待在 Git、KMS 讓最關鍵的金鑰根本不落地。明天回到「服務健康」這個主題——K8S 怎麼知道你的服務是真的活著,還是已經假死?Liveness 與 Readiness Probes 登場。